package net.jalbum.jprint;

import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.CharacterIterator;
import java.util.ArrayList;
import java.util.List;

/**
 * Representation of paintable multi line text
 *
 * @author david
 */
public class TextBox {

    public enum TextAlignment {

        LEFT, CENTERED, RIGHT
    };

    private final AttributedString text;
    private final Graphics2D g2d;
    private TextAlignment alignment = TextAlignment.CENTERED;
    private int lineCount = -1;

    // The LineBreakMeasurer used to line-break the paragraph.
    private LineBreakMeasurer lineMeasurer;

    // index of the first character in the paragraph.
    private int paragraphStart;

    // index of the first character after the end of the paragraph.
    private int paragraphEnd;

    // Explicit line breaks are ignored by the API, so handle them manually
    private Integer[] lineBreakIndexes;

    /**
     * Create a TextBox using a string
     *
     * @param s
     * @param g2d the <code>Graphics2D</code> context into which to render the layout
     */
    public TextBox(String s, Graphics2D g2d) {
        AttributedString as = null;
        if (s != null) {
            as = new AttributedString(s);
            as.addAttribute(TextAttribute.FONT, g2d.getFont());
        }
        this.text = as;
        this.g2d = g2d;
        init(g2d);
    }

    /**
     * Create a TextBox using an AttibutedString
     *
     * @param text
     * @param g2d the <code>Graphics2D</code> context into which to render the layout
     */
    public TextBox(AttributedString text, Graphics2D g2d) {
        this.text = text;
        this.g2d = g2d;
        init(g2d);
    }

    private void init(Graphics2D g2d) {
        // Create a new LineBreakMeasurer from the paragraph.
        // It will be cached and re-used.
        if (lineMeasurer == null && text != null) {
            AttributedCharacterIterator it = text.getIterator();
            paragraphStart = it.getBeginIndex();
            paragraphEnd = it.getEndIndex();
            FontRenderContext frc = g2d.getFontRenderContext();
            lineMeasurer = new LineBreakMeasurer(it, frc);
            lineBreakIndexes = getLineBreakIndexes(text.getIterator());
        }
    }

    protected Integer[] getLineBreakIndexes(AttributedCharacterIterator aci) {
        List<Integer> lineDelimiterList = new ArrayList<Integer>();

        for (char c = aci.first(); c != CharacterIterator.DONE; c = aci.next()) {
            if (c == '\n') {
                lineDelimiterList.add(aci.getIndex());
            }
        }
        lineDelimiterList.add(aci.getEndIndex() - 1);
        return lineDelimiterList.toArray(new Integer[lineDelimiterList.size()]);
    }

    public Dimension getSize(int breakWidth) {
        if (text == null) {
            return new Dimension(0, 0);
        }

        float drawPosY = 0;
        float maxWidth = 0;
        int currentBreak = 0;

        // Set position to the index of the first character in the paragraph.
        lineMeasurer.setPosition(paragraphStart);

        // Get lines until the entire paragraph has been displayed.
        TextLayout layout;
        lineCount = 0;
        while (lineMeasurer.getPosition() < paragraphEnd) {
            lineCount++;

            // Retrieve next layout. A cleverer program would also cache
            // these layouts until the component is re-sized.
            int limit = lineBreakIndexes[currentBreak] + 1;
            layout = lineMeasurer.nextLayout(breakWidth, limit, false);

            if (lineMeasurer.getPosition() == limit) {
                currentBreak++;
            }
            drawPosY += layout.getAscent() + layout.getDescent() + layout.getLeading();

            float lineWidth = layout.getVisibleAdvance();
            if (lineWidth > maxWidth) {
                maxWidth = lineWidth;
            }
        }

        return new Dimension((int) Math.ceil(maxWidth), (int) Math.ceil(drawPosY));
    }

    public int getLineCount(int breakWidth) {
        getSize(breakWidth);
        return lineCount;
    }

    /**
     * Renders this <code>TextLayout</code> at the specified location in the specified
     * {@link java.awt.Graphics2D Graphics2D} context. The origin of the layout is placed at x,&nbsp;y.
     *
     * @param x the X coordinate of the origin of this <code>TextLayout</code>
     * @param y the Y coordinate of the origin of this <code>TextLayout</code>
     * @param breakWidth
     * @see #getBounds()
     */
    public void draw(float x, float y, int breakWidth) {
        AffineTransform backup = g2d.getTransform();
        g2d.translate(x, y);

        if (text == null) {
            return;
        }
        init(g2d);

        float drawPosY = 0;
        int currentBreak = 0;
        
        // Set position to the index of the first character in the paragraph.
        lineMeasurer.setPosition(paragraphStart);

        // Get lines until the entire paragraph has been displayed.
        while (lineMeasurer.getPosition() < paragraphEnd) {

            // Retrieve next layout. A cleverer program would also cache
            // these layouts until the component is re-sized.
            int limit = lineBreakIndexes[currentBreak] + 1;
            TextLayout layout = lineMeasurer.nextLayout(breakWidth, limit, false);

            if (lineMeasurer.getPosition() == limit) {
                currentBreak++;
            }

            // Compute pen x position. If the paragraph is right-to-left we
            // will align the TextLayouts to the right edge of the panel.
            // Note: this won't occur for the English text in this sample.
            // Note: drawPosX is always where the LEFT of the text is placed.
            float drawPosX = layout.isLeftToRight()
                    ? 0 : breakWidth - layout.getAdvance();

            // Move y-coordinate by the ascent of the layout.
            drawPosY += layout.getLeading() + layout.getAscent();

            // Draw the TextLayout at (drawPosX, drawPosY).
            layout.draw(g2d, drawPosX, drawPosY);

            // Move y-coordinate in preparation for next layout.
            drawPosY += layout.getDescent();
        }
        g2d.setTransform(backup);
    }

    public TextAlignment getAlignment() {
        return alignment;
    }

    public void setAlignment(TextAlignment alignment) {
        this.alignment = alignment;
    }

}
